2023-11-22
不看不知道,一看吓一跳,hooks真的需要好好掌握。我刚看了性能优化相关的hooks,发现真的是不用不行啊,如果不用的话,性能方面的问题我还真没有思路来解决。我看过腾讯出品的“扣钉”、用过钉钉开放平台网站,通过浏览器的插件知道它是用react写的,页面更新的时候很卡,说明腾讯、阿里对于react项目的优化也是有进步空间的。
好好学习react hooks,又没有收我的钱,还把笔记给我了,这样的好事真的是要牢牢抓住。可以这样学习:
1、先打开笔记,跟着老师的视频看一遍,千万不要跟着做,因为跟着做非常慢,很容易放弃。
2、看完一遍之后,回忆一下。再看第二遍,这一遍可以跟着做,因为代码有些熟悉了,可以很快写出来,跟着做的同时听老师的讲解。
3、自己看着笔记做一遍,项目应该会创建了,只需要得到效果即可。
4、后面就是不断复习了。
无论如何,明年一定要用上react。
创建其实很简单,只是配置很难,参考:https://cn.vitejs.dev/guide/。预设的模板有这么多,直接输入命令行即可。

直接创建:
xxxxxxxxxx11npm create vite@latest react-test -- --template react-ts其中react-test是项目名称,react-ts是模板名称。
也可以通过命令行选择一步一步的来操作(只需要输入npm create vite@latest,后面按提示操作即可)。(注意:不知道是怎么回事,在git bash里面这样操作是不行的,按箭头键是不起作用的。只能在cmd或powershell里面操作。)

在 Vite 项目中配置 @ 路径提示
xxxxxxxxxx11npm i -D @types/node为什么需要配置@路径提示?
因为这样很方便,在引入外部文件的时候,一般使用的是相对路径,但是相对路径不是很明确,接手的人不容易找到文件所在位置。
如果是按照src目录下来找,就会很明确,依次查找文件即可。
项目创建后,默认使用的就是相对路径。
vite.config.ts 文件:xxxxxxxxxx131// 1. 以 ES6 模块化的方式,从 Node 的 path 模块中,导入 join 函数2import { join } from 'node:path'34// https://vitejs.dev/config/5export default defineConfig({6 plugins: [react()],7 // 2. 在 resolve.alias 对象下,配置 @ 的指向路径8 resolve: {9 alias: {10 '@': join(__dirname, './src/')11 }12 }13})
只需要添加代码即可,原有的代码不需要动。
tsconfig.json 文件,在 compilerOptions 节点下,新增 "baseUrl": "." 和 "paths": { "@/*": [ "src/*" ] } 两项:xxxxxxxxxx401{2 "compilerOptions": {3 /* 新增以下两个配置项,分别是 baseUrl 和 paths */4 "baseUrl": ".",5 "paths": {6 "@/*": [7 "src/*"8 ]9 },10 "target": "ES2020",11 "useDefineForClassFields": true,12 "lib": [13 "ES2020",14 "DOM",15 "DOM.Iterable"16 ],17 "module": "ESNext",18 "skipLibCheck": true,19 /* Bundler mode */20 "moduleResolution": "bundler",21 "allowImportingTsExtensions": true,22 "resolveJsonModule": true,23 "isolatedModules": true,24 "noEmit": true,25 "jsx": "react-jsx",26 /* Linting */27 "strict": true,28 "noUnusedLocals": true,29 "noUnusedParameters": true,30 "noFallthroughCasesInSwitch": true31 },32 "include": [33 "src"34 ],35 "references": [36 {37 "path": "./tsconfig.node.json"38 }39 ]40}这样,就可以使用@来表示/src/目录了。

启动项目,没有问题。
老师是这样用的,将App.tsx里面的组件代码清空,然后创建components文件夹,里面创建不同的文件夹,然后在里面创建不同的组件,通过在App.tsx里面引入这些组件,来讲解不同的hooks用法。这样就非常方便了,不用学习一个hooks就创建一个项目。
在index.css里面更改样式:
启动项目,运行OK:
另外需要注意一点,react+ts项目,编写的组件文件必须是
.tsx后缀,不能是.ts后缀,如果是.ts后缀,会报红色波浪线。
改为
.tsx后缀,就没有问题了。
这应该可以在
tsconfig.json里面进行配置,但是目前我还不知道该怎么配置,先保证不报错即可。-----2024.02.26
应该不能进行配置,因为tsx文件里面就是编写组件的,ts文件无法识别组件的代码,所以会报红色波浪线。
react+ts项目中,vscode如何配置tab键生成标签?
在html中或者vue文件中,都可以使用tab键来生成标签,而且不需要什么配置,但是之前我配置过在react+js项目中使用tab生成标签,现在遇到react+ts项目了,tab键又不起作用了,怎么办?
打开vscode的settings,输入
includeLanguages搜索,在Emmet这里面,添加规则:
我在Indent-Rainbow里面也配置了,在ts文件也有了效果,但对tsx文件没有效果,还不知道怎么配置。
在main.tsx里面,使用了
<React.StrictMode>标签,这个标签的作用是什么呢?
- 检测副作用
- 对于在应用中使用已经废弃、过时的方法会发出警告
对于第二点很好理解,但是第一点是什么意思?因为react全面推进函数式组件,所以“React offers a “Strict Mode” in which it calls each component’s function twice during development.”在开发者模式中,会执行两次组件函数,用来监测组件函数是不是“纯函数”。
那么在react开发过程中,可以把
<React.StrictMode>模式去掉,等到打包时再加上。这样输出时就会显示得好一些。
useState,能让函数组件拥有自己的状态,因此,它是一个管理状态的 hooks API。通过 useState 可以实现状态的初始化、读取、更新。基本语法格式如下:
xxxxxxxxxx11const [状态名, set函数] = useState(初始值)其中:状态名所代表的数据,可以被函数组件使用;如果要修改状态名所代表的数据,需要调用 set 函数 进行修改。例如:
xxxxxxxxxx161import { useState } from 'react'23export function Count() {4 // 定义状态 count,其初始值为 05 // 如果要修改 count 的值,需要调用 setCount(新值) 函数6 const [count, setCount] = useState(0)78 return (9 <>10 <!-- 在函数组件内,使用名为 count 的状态 -->11 <h1>当前的 count 值为:{count}</h1>12 <!-- 点击按钮时,调用 setCount() 函数,为 count 赋新值 -->13 <button onClick={() => setCount(count + 1)}>+1</button>14 </>15 )16}注意:
其实刚开始看到
setCount(count + 1)这种用法的时候,真的不知道是什么意思、该怎么理解、很不习惯,怎么setCount和count一起用了?不一起用不行吗?因为这和普通调用函数的写法非常不一致,普通函数的调用方法是:传入参数即可,操作部分都不需要关心。但是这个函数,似乎应该这样理解:让(count + 1)。这样理解也不对。
首先要理解
count是什么?count就是定义的一个状态值,如果setCount需要用到原来的值,就要使用count,至于count+1,就是一个值而已,然后setCount拿到这个值,为count来赋值。整个流程应该是这样的,把
count+1看成一个结果,不要看成一个过程。
在函数组件中使用 setState 定义状态之后,每当状态发生变化,都会触发函数组件的重新执行,从而根据最新的数据更新渲染 DOM 结构。例如:
xxxxxxxxxx231import { useState } from 'react'23export function Count() {4 // 定义状态 count,其初始值为 05 // 如果要修改 count 的值,需要调用 setCount(新值) 函数6 const [count, setCount] = useState(0)78 // 每次 count 值发生变化,都会打印下面的这句话:9 console.log('组件被重新渲染了')1011 const add = () => {12 setCount(count + 1)13 }1415 return (16 <>17 <!-- 在函数组件内,使用名为 count 的状态 -->18 <h1>当前的 count 值为:{count}</h1>19 <!-- 点击按钮时,在 add 处理函数中,调用 setCount() 函数,为 count 赋新值 -->20 <button onClick={add}>+1</button>21 </>22 )23}注意:当函数式组件被重新执行时,不会重复调用 useState() 给数据赋初值,而是会复用上次的 state 值。
在使用 useState 定义状态时,除了可以直接给定初始值,还可以通过函数返回值的形式,为状态赋初始值,语法格式如下:
xxxxxxxxxx11const [value, setValue] = useState(() => 初始值)例如:
xxxxxxxxxx161export const DateCom: React.FC = () => {2 // const [date] = useState({ year: 2023, month: 9, day: 11 })3 const [date, setDate] = useState(() => {4 const dt = new Date()5 return { year: dt.getFullYear(), month: dt.getMonth() + 1, day: dt.getDate() }6 })78 return (9 <>10 <h1>今日信息:</h1>11 <p>年份:{date.year}年</p>12 <p>月份:{date.month}月</p>13 <p>日期:{date.day}日</p>14 </>15 )16}注意:以函数的形式为状态赋初始值时,只有组件首次被渲染才会执行 fn 函数;当组件被更新时,会以更新前的值作为状态的初始值,赋初始值的函数不会执行。
调用 useState() 会返回一个变更状态的函数,这个函数内部是以异步的形式修改状态的,所以修改状态后无法立即拿到最新的状态,例如:
xxxxxxxxxx171export const Count: React.FC = () => {2 const [count, setCount] = useState(() => 0)34 const add = () => {5 // 1. 让数值自增+16 setCount(count + 1)7 // 2. 打印 count 的值8 console.log(count)9 }1011 return (12 <>13 <h1>当前的 count 值为:{count}</h1>14 <button onClick={add}>+1</button>15 </>16 )17}在上述代码的第8行,打印出来的 count 值是更新前的旧值,而非更新后的新值。证明 useState 是异步变更状态的。

为了能够监听到状态的变化,react 提供了 useEffect 函数。它能够监听依赖项状态的变化,并执行对应的回调函数。基本语法格式如下:
xxxxxxxxxx11useEffect(() => { /* 依赖项变化时,要触发的回调函数 */ }, [依赖项])例如:
xxxxxxxxxx191export const Count: React.FC = () => {2 const [count, setCount] = useState(() => 0)34 const add = () => {5 setCount(count + 1)6 }78 // 当 count 变化后,会触发 useEffect 指定的回调函数9 useEffect(() => {10 console.log(count)11 }, [count])1213 return (14 <>15 <h1>当前的 count 值为:{count}</h1>16 <button onClick={add}>+1</button>17 </>18 )19}

注意:useEffect 也是 React 提供的 Hooks API,后面的课程中会对它进行详细的介绍。
如果要更新对象类型的值,并触发组件的重新渲染,则必须使用展开运算符或Object.assign()生成一个新对象,用新对象覆盖旧对象,才能正常触发组件的重新渲染。示例代码如下:
xxxxxxxxxx351export const UserInfo: React.FC = () => {2 const [user, setUser] = useState({3 name: 'zs',4 age: 12,5 gender: '男'6 })78 const updateUserInfo = () => {9 // 首先修改对象的值,然后使用 setXXX 函数。10 user.name = 'Jesse Pinkman'11 12 // 下面的写法是错误的,因为 set 函数内部,会对更新前后的值进行对比;13 // 由于更新前后的 user,原值的引用和新值的引用相同,14 // 所以 react 认为值没有发生变化,不会触发组件的重新渲染。15 // setUser(user)1617 // 解决方案:用新对象的引用替换旧对象的引用,即可正常触发组件的重新渲染。18 setUser({ user })19 // setUser(Object.assign({}, user))20 21 // 也可以这样写22 setUser({user, name: "Jesse Pinkman"})23 }2425 return (26 <>27 <h1>用户信息:</h1>28 <p>姓名:{user.name}</p>29 <p>年龄:{user.age}</p>30 <p>性别:{user.gender}</p>3132 <button onClick={updateUserInfo}>更新用户信息</button>33 </>34 )35}当连续多次以相同的操作更新状态值时,React 内部会对传递过来的新值进行比较,如果值相同,则会屏蔽后续的更新行为,从而防止组件频繁渲染的问题。这虽然提高了性能,但也带来了一个使用误区,例如:
xxxxxxxxxx171export const Count: React.FC = () => {2 const [count, setCount] = useState(() => 0)34 const add = () => {5 // 1. 希望让 count 值从 0 自增到 16 setCount(count + 1)7 // 2. 希望让 count 值从 1 自增到 28 setCount(count + 1)9 }1011 return (12 <>13 <h1>当前的 count 值为:{count}</h1>14 <button onClick={add}>+1</button>15 </>16 )17}经过测试,我们发现上述代码执行的结果,只是让 count 从 0 变成了 1,最终的 count 值并不是 2。Why?
因为 setCount 是异步地更新状态值的,那么前后两次调用 setCount 的代码执行是同步的,传递到异步更新队列里面的新值都是 1。React 内部如果遇到两次相同的状态,则会默认阻止组件再次更新。
为了解决上述的问题,我们可以使用函数的方式给状态赋新值。当函数执行时才通过函数的形参,拿到当前的状态值,并基于它返回新的状态值。示例代码如下:
xxxxxxxxxx161export const Count: React.FC = () => {2 const [count, setCount] = useState(() => 0)34 const add = () => {5 // 传递了更新状态的函数进去。这里的c是形参,更有意义的命名应该是 prev ,表示setCount执行之前状态的值,要理解这一点。6 setCount((c) => c + 1)7 setCount((c) => c + 1)8 }910 return (11 <>12 <h1>当前的 count 值为:{count}</h1>13 <button onClick={add}>+1</button>14 </>15 )16}在函数组件中,我们可以通过 useState 来模拟 forceUpdate 的强制刷新操作。因为只要 useState 的状态发生了变化,就会触发函数组件的重新渲染,从而达到强制刷新的目的。具体的代码示例如下:
xxxxxxxxxx131export const FUpdate: React.FC = () => {2 const [, forceUpdate] = useState({})34 // 每次调用 onRefresh 函数,都会给 forceUpdate 传递一个新对象5 // 从而触发组件的重新渲染6 const onRefresh = () => forceUpdate({})78 return (9 <>10 <button onClick={onRefresh}>点击强制刷新 --- {Date.now()}</button>11 </>12 )13}
注意:因为每次传入的对象的地址不同,所以一定会使组件刷新。
useRef 函数返回一个可变的 ref 对象,该对象只有一个 current 属性。可以在调用 useRef 函数时为其指定初始值。并且这个返回的 ref 对象在组件的整个生命周期内保持不变。语法格式如下:
xxxxxxxxxx61// 1. 导入 useRef2import { useRef } from 'react'3// 2. 调用 useRef 创建 ref 对象。如果是操作DOM元素或子组件,一般初始值设置为null4const refObj = useRef(初始值)5// 3. 通过 ref.current 访问 ref 中存储的值6console.log(refObj.current)useRef 函数用来解决以下两个问题:
下面的代码演示了如何获取 Input 元素的实例,并调用其 DOM API。
xxxxxxxxxx191import React, { useRef } from 'react'23export const InputFocus: React.FC = () => {4 // 1. 创建 ref 引用5 const iptRef = useRef<HTMLInputElement>(null)67 const getFocus = () => {8 // 3. 调用 focus API,让文本框获取焦点9 iptRef.current?.focus()10 }1112 return (13 <>14 {/* 2. 绑定 ref 引用 */}15 <input type="text" ref={iptRef} />16 <button onClick={getFocus}>点击获取焦点</button>17 </>18 )19}
上面在使用useRef的时候,定义了类型
const iptRef = useRef<HTMLInputElement>(null),其实我对这些类型还是很害怕的,因为react的一些类型(不只是react,还有vue项目)我都不知道该到哪里去找,原来想好的方法也就是遇见一个记一个,但这很显然不能减少我的恐惧。在这个项目中,有类型提示了,多多少少算是一种便利。
基于 useRef 创建名为 prevCountRef 的数据对象,用来存储上一次的旧 count 值。每当点击按钮触发 count 自增时,都把最新的旧值赋值给 prevCountRef.current 即可:
xxxxxxxxxx231export const Counter: React.FC = () => {2 // 默认值为 03 const [count, setCount] = useState(0)45 // 默认值为 undefined6 const prevCountRef = useRef<number>()78 const add = () => {9 // 点击按钮时,让 count 值异步 +110 setCount((c) => c + 1)11 // 同时,把 count 所代表的旧值记录到 prevCountRef 中12 prevCountRef.current = count13 }1415 return (16 <>17 <h1>18 新值是:{count},旧值是:{prevCountRef.current}19 </h1>20 <button onClick={add}>+1</button>21 </>22 )23}
在 RefTimer 组件中,点击 +1 按钮,会让 count 值自增,从而触发 RefTimer 组件的 rerender。但是,我们发现 RefTimer 组件中的时间戳保持不变,这说明组件每次渲染,不会重复调用 useRef 函数进行初始化。示例代码如下:
xxxxxxxxxx151export const RefTimer: React.FC = () => {2 const [count, setCount] = useState(0)3 const time = useRef(Date.now())45 console.log('组件被渲染了')67 return (8 <>9 <h3>10 count值是:{count}, 时间戳是:{time.current}11 </h3>12 <button onClick={() => setCount((prev) => prev + 1)}>+1</button>13 </>14 )15}
点击给 ref 赋新值的按钮时,为 time.current 赋新值,执行的结果是:
time.current 的值这证明了 ref.current 变化时不会造成组件的 rerender,示例代码如下:
xxxxxxxxxx211export const RefTimer: React.FC = () => {2 const [count, setCount] = useState(0)3 const time = useRef(Date.now())45 const updateTime = () => {6 time.current = Date.now()7 console.log(time.current)8 }910 console.log('组件被渲染了')1112 return (13 <>14 <h3>15 count值是:{count}, 时间戳是:{time.current}16 </h3>17 <button onClick={() => setCount((prev) => prev + 1)}>+1</button>18 <button onClick={updateTime}>给ref赋新值</button>19 </>20 )21}
react迷人之处
这个看上去很难理解,因为updateTime和console都在同一个函数中,为什么执行了updateTime,而它的下一行代码console就不会执行呢?
这完全超乎我的经验和想象。这就是JS和react的迷人之处了,不知道react是怎么处理的,代码粒度非常细,估计是将updateTime抽离出去,放到不同的流程来执行。
由于 ref.current 值的变化不会造成组件的 rerender,而且 React 也不会跟踪 ref.current 的变化,因此 ref.current 不可以作为其它 hooks(useMemo、useCallback、useEffect 等) 的依赖项。
xxxxxxxxxx251export const RefTimer: React.FC = () => {2 const [count, setCount] = useState(0)3 const time = useRef(Date.now())45 const updateTime = () => {6 time.current = Date.now()7 console.log(time.current)8 }910 console.log('组件被渲染了')1112 useEffect(() => {13 console.log('time 的值发生了变化:' + time.current)14 }, [time.current])1516 return (17 <>18 <h3>19 count值是:{count}, 时间戳是:{time.current}20 </h3>21 <button onClick={() => setCount((prev) => prev + 1)}>+1</button>22 <button onClick={updateTime}>给ref赋新值</button>23 </>24 )25}
在上面的代码中,组件首次渲染完成后,必然会触发一次 useEffect 的执行。但是,当 time.current 发生变化时,并不会触发 useEffect 的重新执行。因此,不能把 ref.current 作为其它 hooks 的依赖项。
当使用ref.current作为其它hooks的依赖项时,vscode会提示:

ref 的作用是获取实例,但由于函数组件不存在实例,因此无法通过 ref 获取函数组件的实例引用。而 React.forwardRef 就是用来解决这个问题的。(之前学习useRef的时候,如果是指定为元素的ref属性,都是指定为已知的html元素上面的,但是不能指定到自定义的函数式组件上。)
React.forwardRef 会创建一个 React 组件,这个组件能够将其接收到的 ref 属性转发到自己的组件树。
注意:
React.forwardRef不是hooks,而是react的一个api。
在下面的例子中,父组件 Father 想通过 ref 引用子组件 Child,此时代码会报错,因为函数式组件没有实例对象,无法被直接引用:
xxxxxxxxxx151// 父组件2export const Father: React.FC = () => {3 const childRef = useRef()45 console.log(childRef)6 7 return (8 <>9 <h1>Father 父组件</h1>10 <hr />11 <!-- 下面这行代码中的 ref 使用不正确,因为 Child 组件是函数式组件,无法被直接引用 -->12 <Child ref={childRef} />13 </>14 )15}Child 组件的定义如下:
xxxxxxxxxx161// 子组件(实现点击按钮,数值加减的操作)2const Child: React.FC = () => {3 const [count, setCount] = useState(0)45 const add = (step: number) => {6 setCount((prev) => (prev += step))7 }89 return (10 <>11 <h3>Child 子组件 {count}</h3>12 <button onClick={() => add(-1)}>-1</button>13 <button onClick={() => add(1)}>+1</button>14 </>15 )16}注意:上面的代码可以运行,但会在终端提示如下的 Warning 警告:

xxxxxxxxxx31Warning:2Function components cannot be given refs. Attempts to access this ref will fail.3Did you mean to use React.forwardRef()?
父组件中输出的内容为:{ current: null },说明是无法拿到Child组件的ref的。
错误提示中有解决此问题的关键提示:Did you mean to use React.forwardRef()?
在使用函数组件时,我们无法直接使用 ref 引用函数式组件,下面的代码会产生报错:
xxxxxxxxxx21const childRef = useRef(null)2return <Child ref={childRef} />因为默认情况下,你自己的组件不会暴露它们内部 DOM 节点的 ref。
正确的方法是使用 React.forwardRef() 把函数式组件包裹起来(只需要包裹起来即可,该怎么定义函数式组件就怎么定义。),例如 Child 子组件的代码如下:
xxxxxxxxxx61import * as React from "react"23// 被包装的函数式组件,第一个参数是 props,第二个参数是转发过来的 ref4const Child = React.forwardRef((props, ref) => {5 // 省略子组件内部的具体实现6})xxxxxxxxxx161const Child = React.forwardRef((props, ref) => {2 const [count, setCount] = useState(0);34 const add = (step: number) => {5 setCount((prev) => (prev += step));6 };7 return (8 <>9 <h3>Child 子组件 {count}</h3>10 <button onClick={() => add(-1)} style={{ background: "orange" }}>11 点我-112 </button>13 <button onClick={() => add(1)}>点我+1</button>14 </>15 );16});可以直接从react中导入forwardRef方法:
xxxxxxxxxx11import { forwardRef } from "react"
注意:当使用React.forwardRef定义组件之后,就不能将Child组件的类型定义为React.FC了,但是定义为什么类型,我看到老师的vscode里面好像有提示,但是我的vscode里面是没有提示的,所以暂时不管吧。
-----2024.02.26
查看react官方的ts定义,forwardRef需要传递两个泛型参数进去,那T和P应该怎么写呢?继续查看ForwardRefRenderFunction里面的定义:
可以看到,P就可以定义为自定义的对象类型,但是T还是不明确,继续查看ForwardedRef里面是怎么定义T的:
还是不明确T的类型定义,继续查看MutableRefObject是怎么定义T的:
从这个定义来看,似乎T可以随便定义,那这个案例这里怎么定义呢?
确实是可以随便定义的,是根据ref的类型来定义的,T决定了ref的类型,需要注意的是,函数中定义泛型和使用参数的顺序是反的,所以泛型第一个参数是T,即ref的类型,第二个泛型参数是P,可以省略:
xxxxxxxxxx491import { FC, useRef, useState, useImperativeHandle } from 'react'2import * as React from "react"34type ChildRefType = {5count: number;6reset: () => void;7} | null;89export const Father: FC = () => {10const childRef = useRef<ChildRefType>(null)1112const showRef = () => {13console.log(childRef)14}1516return (17<>18<h1>Father组件</h1>19<hr />20<button onClick={showRef}>Show Ref</button>21<button onClick={() => childRef.current!.reset()}>reset</button>22<Child ref={childRef} />23</>24)25}2627const Child = React.forwardRef<ChildRefType>(28(_, ref) => {29const [count, setCount] = useState(0)30const [flag, setFlag] = useState(false)31useImperativeHandle(ref, () => {32console.log('执行了 useImpetativeHandle 的回调')33return {34count,35reset: () => setCount(0)36}37})3839return (40<>41<h3>Child子组件,count的值为:{count},flag的值为:{JSON.stringify(flag)}</h3>42<button onClick={() => setCount(prev => prev + 1)}>+1</button>43<button onClick={() => setCount(prev => prev - 1)}>-1</button>4445<button onClick={() => setFlag(prev => !prev)}>Toggle</button>46</>47)48}49)其实我之前都做出来了,类型都写对了,但是
<Child ref={childRef} />的ref下面一直有红色波浪线,搞得我找不到错在哪里,它的报错信息是这样的,我也看不懂:
后来发现,useRef里面没有赋初始值,改成这样之后就没有报错了:
useRef(null)。其实我应该看懂的,里面有一句:Type 'ChildRefType | undefined' is not assignable to type 'ChildRefType | null'. Type 'undefined' is not assignable to type 'ChildRefType | null'.ts(2322)应该已经提示我了,undefined类型对不上null类型,应该显式的赋值为null。阮一峰的教程里面应该讲到了。
然后,在父组件 Father 中,就可以给子组件 Child 绑定 ref 了:
xxxxxxxxxx191// 父组件2export const Father: React.FC = () => {3 const childRef = useRef(null)45 // 按钮的点击事件处理函数6 const onShowRef = () => {7 console.log(childRef.current)8 }910 return (11 <>12 <h1>Father 父组件</h1>13 {/* 点击按钮,打印 ref 的值 */}14 <button onClick={onShowRef}>show Ref</button>15 <hr />16 <Child ref={childRef} />17 </>18 )19}注意:此时父组件 Father 中获取到的 ref.current 是 null,因为子组件 Child 没有向外暴露任何自己内部的东西。
怎么向外暴露子组件里面的DOM呢?将ref传递到要公开的DOM结点中即可:
xxxxxxxxxx141const Child = React.forwardRef<HTMLInputElement>(2(props, ref) => {3const [count, setCount] = useState(0)45return (6<>7<h3>Child子组件,count的值为:{count}</h3>8<input type="text" ref={ref} />9<button onClick={() => setCount(prev => prev + 1)}>+1</button>10<button onClick={() => setCount(prev => prev - 1)}>-1</button>11</>12)13}14)在Father组件中,获取实例:
xxxxxxxxxx171export const Father: FC = () => {2const childRef = useRef<HTMLInputElement>(null)34const showRef = () => {5console.log(childRef)6childRef.current!.focus();7}89return (10<>11<h1>Father组件</h1>12<hr />13<button onClick={showRef}>Show Ref</button>14<Child ref={childRef} />15</>16)17}效果:
直接使用 ref 获取 DOM 实例,会全面暴露 DOM 实例上的 API,从而导致外部使用 ref 时有更大的自由度。在实际开发中,我们应该严格控制 ref 的暴露颗粒度,控制它能调用的方法,只向外暴露主要的功能函数,其它功能函数不暴露。
React 官方提供 useImperativeHandle 的目的,就是让你在使用 ref 时可以自定义暴露给外部组件哪些功能函数或属性。
它的语法结构如下:
xxxxxxxxxx11useImperativeHandle(通过forwardRef接收到的父组件的ref对象, () => 自定义ref对象, [依赖项数组])其中,第三个参数(依赖项数组)是可选的。
第一个参数,就是使用React.forwardRef定义函数式组件时,会有两个参数,第一个参数是props,第二个参数是ref。那么这里的第一个参数就是ref,直接拿过来就行了。
第二个参数就是需要暴露出去的对象,写成了函数返回值的形式,注意一定要返回值。
在被 React.forwardRef() 包裹的组件中,需要结合 useImperativeHandle 这个 hooks API,向外按需暴露子组件内的成员:
xxxxxxxxxx321import React, { useRef, useState, useImperativeHandle } from 'react'23// 子组件45// 注意参数的写法,第一个参数本来是props,但是组件里面暂时用不到props,如果写成props,ts的提示会有波浪线。如果写成 _ ,就不会有波浪线的提示。6const Child = React.forwardRef((_, ref) => {7 8 const [count, setCount] = useState(0)910 const add = (step: number) => {11 setCount((prev) => (prev += step))12 }1314 // 向外暴露一个空对象,可以这么写:15 // useImperativeHandle(ref, () => ({}))16 17 // 注意这里的写法,第二个参数是要返回一个ref对象,所以写成了箭头函数返回对象字面量的形式,参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions#%E8%BF%94%E5%9B%9E%E5%AF%B9%E8%B1%A1%E5%AD%97%E9%9D%A2%E9%87%8F18 19 // 向外暴露一个对象,其中包含了 name 和 age 两个属性20 useImperativeHandle(ref, () => ({21 name: 'liulongbin',22 age: 2223 }))2425 return (26 <>27 <h3>Child 子组件 {count}</h3>28 <button onClick={() => add(-1)}>-1</button>29 <button onClick={() => add(1)}>+1</button>30 </>31 )32})打印结果:

在子组件中,向外暴露 count 和 setCount 这两个成员:
xxxxxxxxxx221// 子组件2const Child = React.forwardRef((_, ref) => {3 const [count, setCount] = useState(0)45 const add = (step: number) => {6 setCount((prev) => (prev += step))7 }89 // 向外暴露 count 的值和 setCount 函数10 useImperativeHandle(ref, () => ({11 count,12 setCount13 }))1415 return (16 <>17 <h3>Child 子组件 {count}</h3>18 <button onClick={() => add(-1)}>-1</button>19 <button onClick={() => add(1)}>+1</button>20 </>21 )22})在父组件中,添加一个重置按钮,当点击重置按钮时,调用 ref 向外暴露的 setCount 函数,把子组件内部的 count 重置为 0。示例代码如下:
xxxxxxxxxx271// 父组件2export const Father: React.FC = () => {3 // 注意:这里为ref定义了类型,是根据 useImperativeHandle 传递过来的ref来定义的。4 const childRef = useRef<{ count: number; setCount: (value: number) => void }>(null)56 // 按钮的点击事件处理函数7 const onShowRef = () => {8 console.log(childRef.current)9 }1011 // 重置按钮的点击事件处理函数12 const onReset = () => {13 childRef.current?.setCount(0)14 }1516 return (17 <>18 <h1>Father 父组件</h1>19 {/* 点击按钮,打印 ref 的值 */}20 <button onClick={onShowRef}>show Ref</button>21 {/* 点击按钮,重置数据为 0 */}22 <button onClick={onReset}>重置</button>23 <hr />24 <Child ref={childRef} />25 </>26 )27}
在 Child 子组件中,我们希望对外暴露一个重置 count 为 0 的函数,而不希望直接把 setCount() 暴露出去,因为父组件调用 setCount() 时可以传任何数值,这样又要写校验的代码,很麻烦。因此,我们可以基于 useImperativeHandle,向外提供一个 reset() 函数而非直接把 setCount() 暴露出去:
xxxxxxxxxx231// 子组件2const Child = React.forwardRef<{ count: number; setCount: (value: number) => void }>((_, ref) => {3 const [count, setCount] = useState(0)45 const add = (step: number) => {6 setCount((prev) => (prev += step))7 }89 // 向外暴露 count 的值和 reset 函数10 useImperativeHandle(ref, () => ({11 count,12 // 在组件内部封装一个重置为 0 的函数,API 的粒度更小13 reset: () => setCount(0)14 }))1516 return (17 <>18 <h3>Child 子组件 {count}</h3>19 <button onClick={() => add(-1)}>-1</button>20 <button onClick={() => add(1)}>+1</button>21 </>22 )23})在父组件中,调用 ref.current.reset() 即可把数据重置为 0:
xxxxxxxxxx261// 父组件2export const Father: React.FC = () => {3 const childRef = useRef<{ count: number; reset: () => void }>(null)45 // 按钮的点击事件处理函数6 const onShowRef = () => {7 console.log(childRef.current)8 }910 // 重置按钮的点击事件处理函数11 const onReset = () => {12 childRef.current?.reset()13 }1415 return (16 <>17 <h1>Father 父组件</h1>18 {/* 点击按钮,打印 ref 的值 */}19 <button onClick={onShowRef}>show Ref</button>20 {/* 点击按钮,重置数据为 0 */}21 <button onClick={onReset}>重置</button>22 <hr />23 <Child ref={childRef} />24 </>25 )26}再来回顾一下 useImperativeHandle 的参数项:
xxxxxxxxxx11useImperativeHandle(ref, createHandle, [deps])其中,第三个参数有3种用法:
空数组:只在子组件首次被渲染时,执行 useImperativeHandle 中的 fn 回调,从而把 return 的对象作为父组件接收到的 ref。例如:
xxxxxxxxxx331import React, { useState, useImperativeHandle } from 'react'23// 子组件4const Child = React.forwardRef((_, ref) => {5 const [count, setCount] = useState(0)67 const add = (step: number) => {8 setCount((prev) => (prev += step))9 }1011 // 向外暴露 count 的值和 reset 函数12 useImperativeHandle(13 ref,14 () => {15 // 这个 console 只执行1次,哪怕 count 值更新了,也不会重新执行16 // 导致的结果是:外界拿到的 count 值,永远是组件首次渲染时的初始值 017 console.log('执行了 useImperativeHandle 的回调')18 return {19 count,20 reset: () => setCount(0)21 }22 },23 []24 )2526 return (27 <>28 <h3>Child 子组件 {count}</h3>29 <button onClick={() => add(-1)}>-1</button>30 <button onClick={() => add(1)}>+1</button>31 </>32 )33})
可以看到,外界拿到的count值,永远是初次渲染时的值。
依赖项数组:子组件首次被渲染时,或依赖项改变时,会执行 useImperativeHandle 中的 fn 回调,从而让父组件通过 ref 能拿到依赖项的新值。例如:
xxxxxxxxxx391import React, { useState, useImperativeHandle } from 'react'23// 子组件4const Child = React.forwardRef((_, ref) => {5 const [count, setCount] = useState(0)6 const [flag, setFlag] = useState(false)78 const add = (step: number) => {9 setCount((prev) => (prev += step))10 }1112 // 向外暴露 count 的值和 reset 函数13 useImperativeHandle(14 ref,15 () => {16 // 每当依赖项 count 值变化,都会触发这个回调函数的重新执行17 // 因此,父组件能拿到变化后的最新的 count 值18 console.log('执行了 useImperativeHandle 的回调')19 return {20 count,21 reset: () => setCount(0)22 }23 },24 // 注意:只有 count 值变化,才会触发回调函数的重新执行25 // flag 值的变化,不会导致回调函数的重新执行,因为 flag 没有被声明为依赖项26 [count]27 )2829 return (30 <>31 <h3>Child 子组件 {count}</h3>32 <p>flag 的值是:{String(flag)}</p>33 <button onClick={() => add(-1)}>-1</button>34 <button onClick={() => add(1)}>+1</button>35 {/* 点击按钮,切换布尔值 */}36 <button onClick={() => setFlag((prev) => !prev)}>Toggle</button>37 </>38 )39})
省略依赖项数组(省略第三个参数):此时,组件内任何 state 的变化,都会导致 useImperativeHandle 中的回调的重新执行。因此如果使用useImperativeHandle,那么第三个参数不能省略,否则会导致性能问题,除非是特殊目的。示例代码如下:
xxxxxxxxxx321import React, { useState, useImperativeHandle } from 'react'23// 子组件4const Child = React.forwardRef((_, ref) => {5 const [count, setCount] = useState(0)6 const [flag, setFlag] = useState(false)78 const add = (step: number) => {9 setCount((prev) => (prev += step))10 }1112 // 向外暴露 count 的值和 reset 函数13 useImperativeHandle(ref, () => {14 // 只要组件内的任何 state 发生变化,都会触发回调函数的重新执行15 console.log('执行了 useImperativeHandle 的回调')16 return {17 count,18 reset: () => setCount(0)19 }20 })2122 return (23 <>24 <h3>Child 子组件 {count}</h3>25 <p>flag 的值是:{String(flag)}</p>26 <button onClick={() => add(-1)}>-1</button>27 <button onClick={() => add(1)}>+1</button>28 {/* 点击按钮,切换布尔值 */}29 <button onClick={() => setFlag((boo) => !boo)}>Toggle</button>30 </>31 )32})
陷阱1:不要滥用 ref。 父子、祖孙的传值、传方法,首先就应该想到props,这是最简单的方法,也是react最推荐的方法。
你应当仅在你没法通过 prop 来表达 命令式 行为的时候才使用 ref:例如,滚动到指定节点、聚焦某个节点、触发一次动画,以及选择文本等等。
陷阱2:如果可以通过 prop 实现,那就不应该使用 ref。例如,你不应该从一个
Model组件暴露出{open, close}这样的命令式句柄,最好是像<Modal isOpen={isOpen} />这样,将isOpen作为一个 prop。副作用 可以帮你通过 prop 来暴露一些命令式的行为。